// 声明这个文件属于 main 包。在 Go 语言中，一个可执行的程序必须有一个 main 包，
// 并且在这个包里必须有一个 main 函数，这是程序的入口点。
package main

// 导入一系列 Go 语言的标准库和第三方库。
import (
	"bufio"   // 提供了带缓冲的 I/O 操作，比如逐行读取。
	"fmt"     // 包含了格式化 I/O 的函数，类似于 C 的 printf 和 scanf。
	"io"      // 提供了基本的 I/O 原语接口。
	"log"     // 实现了简单的日志服务。
	"os"      // 提供了平台无关的操作系统功能接口。
	"os/exec" // 提供了运行外部命令的功能。
	"sync"    // 提供了基本的同步原语，如互斥锁和 WaitGroup。

	"gopkg.in/yaml.v2" // 这是一个第三方库，用于解析 YAML 文件。
)

// GatewayConfig 结构体定义了网关的主要配置项。
// `yaml:"..."` 这种标记 (tag) 是给 `gopkg.in/yaml.v2` 库看的，
// 它告诉库在解析 YAML 文件时，如何将 YAML 的字段名映射到 Go 结构体的字段上。
type GatewayConfig struct {
	Server   ServerConfig    `yaml:"server"`   // 服务器相关配置
	Logging  LoggingConfig   `yaml:"logging"`  // 日志相关配置
	Features FeaturesConfig  `yaml:"features"` // 功能开关配置
	Services []ServiceConfig `yaml:"services"` // 要管理的服务列表
}

// ServerConfig 结构体定义了服务器的配置。
type ServerConfig struct {
	Host              string `yaml:"host"`                // 主机地址
	Port              int    `yaml:"port"`                // 端口号
	MaxConnectionIdle string `yaml:"max_connection_idle"` // 最大空闲连接时间
	Timeout           string `yaml:"timeout"`             // 超时时间
}

// LoggingConfig 结构体定义了日志的配置。
type LoggingConfig struct {
	Level  string `yaml:"level"`  // 日志级别 (e.g., "info", "debug")
	Format string `yaml:"format"` // 日志格式 (e.g., "json", "text")
	Output string `yaml:"output"` // 日志输出位置 (e.g., "stdout")
}

// FeaturesConfig 结构体定义了功能开关。
type FeaturesConfig struct {
	EnableMetrics bool `yaml:"enable_metrics"` // 是否启用监控
	EnableTracing bool `yaml:"enable_tracing"` // 是否启用追踪
}

// ServiceConfig 结构体定义了单个服务的配置。
type ServiceConfig struct {
	Description string   `yaml:"description"` // 服务的描述
	Host        string   `yaml:"host"`        // 服务的主机 (在此示例中未使用，但保留)
	Port        int      `yaml:"port"`        // 服务的端口 (在此示例中未使用，但保留)
	Timeout     string   `yaml:"timeout"`     // 服务的超时 (在此示例中未使用，但保留)
	Start       bool     `yaml:"start"`       // 是否启动此服务
	Exec        string   `yaml:"exec"`        // 要执行的命令或可执行文件的路径
	Args        []string `yaml:"args"`        // 传递给可执行文件的参数列表
	LogFile     string   `yaml:"log_file"`    // 指定日志输出的文件路径
	Env         []string `yaml:"env"`         // 为此服务设置的环境变量
}

// Config 结构体是整个 YAML 配置文件的顶层结构。
type Config struct {
	Gateway GatewayConfig `yaml:"gateway_config"` // 对应 YAML 文件中的 `gateway_config` 顶层键
}

// main 函数是整个程序的入口。
func main() {
	// 使用 os.ReadFile 读取 config.yaml 文件的全部内容。
	// 这是 Go 1.16 及以后版本推荐的做法。
	// 它返回文件内容的字节切片和一个可能的错误。
	yamlFile, err := os.ReadFile("config.yaml")
	if err != nil {
		// 如果读取文件出错 (e.g., 文件不存在)，程序会用 log.Fatalf 打印错误信息并立即退出。
		log.Fatalf("错误：读取 YAML 文件失败: %s\n", err)
	}

	// 声明一个 Config 类型的变量 config，用于存储解析后的配置数据。
	var config Config
	// 使用 yaml.Unmarshal 函数将读取到的 YAML 文件内容 (yamlFile) 解析到 config 结构体中。
	// `&config` 是取 config 变量的内存地址，因为 Unmarshal 函数需要修改 config 的内容。
	err = yaml.Unmarshal(yamlFile, &config)
	if err != nil {
		// 如果 YAML 格式不正确或与结构体定义不匹配，解析会失败。
		log.Fatalf("错误：解析 YAML 失败: %s\n", err)
	}

	// 声明一个 sync.WaitGroup 变量 wg。
	// WaitGroup 用于等待一组 Go 协程 (goroutine) 完成。
	// 主协程 (main 函数) 会调用 wg.Add() 设置需要等待的协程数量，
	// 每个子协程在结束时调用 wg.Done()，
	// 主协程通过调用 wg.Wait() 来阻塞自己，直到所有子协程都完成了。
	var wg sync.WaitGroup

	// 使用 for-range 循环遍历解析出的服务列表 (config.Gateway.Services)。
	// `_` (下划线) 是一个空白标识符，表示我们忽略循环中的索引。
	for _, service := range config.Gateway.Services {
		// 检查配置文件中该服务的 `start` 字段是否为 true。
		if service.Start {
			// 如果为 true，调用 wg.Add(1) 来增加等待计数，表示我们将启动一个新的协程。
			wg.Add(1)
			// 使用 `go` 关键字启动一个新的协程来执行 startService 函数。
			// Go 协程是轻量级的线程，由 Go 运行时管理。
			// 这样可以并发地启动和管理所有服务，而不会阻塞主流程。
			go startService(service, &wg)
		}
	}

	// 调用 wg.Wait()，主协程将在这里暂停，直到所有通过 wg.Add(1) 注册的协程
	// 都调用了 wg.Done()。这确保了主程序不会在所有服务启动完成前退出。
	wg.Wait()
	// 当所有服务的协程都结束后，打印这条信息。
	fmt.Println("所有指定的服务都已处理完毕。")
}

// startService 函数负责启动和监控一个具体服务。
// 它接收一个 ServiceConfig 结构体和一个指向 WaitGroup 的指针。
// 在函数开头使用 defer wg.Done() 是一个很好的实践。
// `defer` 语句会将其后的函数调用推迟到当前函数执行完毕前执行。
// 这确保了无论函数是正常返回还是因为错误而提前结束，wg.Done() 都会被调用，
// 从而避免主协程永久等待。
func startService(service ServiceConfig, wg *sync.WaitGroup) {
	defer wg.Done()

	// 使用 exec.Command 创建一个 *exec.Cmd 对象，用于执行外部命令。
	// 第一个参数是可执行文件的路径，后续参数是传递给该程序的命令行参数。
	// `service.Args...` 是一种语法糖，它会将 `service.Args` 这个切片 (slice)
	// 中的所有元素展开，作为独立的参数传递给 Command 函数。
	cmd := exec.Command(service.Exec, service.Args...)

	// 如果服务配置中指定了环境变量 (Env)，则为这个即将执行的命令设置环境变量。
	if len(service.Env) > 0 {
		// cmd.Env 默认是 nil，表示继承当前进程的环境变量。
		// 为了在继承的基础上添加新的环境变量，我们首先获取当前进程的所有环境变量。
		cmd.Env = os.Environ()
		// 然后将配置文件中指定的环境变量追加进去。
		cmd.Env = append(cmd.Env, service.Env...)
	}

	// 为命令的标准输出 (stdout) 创建一个管道 (pipe)。
	// 管道是一个单向的数据通道，一端连接到子进程的输出，另一端可以在当前 Go 程序中读取。
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		log.Printf("错误：为服务 %s 创建标准输出管道失败: %s\n", service.Description, err)
		return // 如果创建失败，则记录日志并提前返回。
	}
	// 同样地，为命令的标准错误 (stderr) 创建一个管道。
	stderr, err := cmd.StderrPipe()
	if err != nil {
		log.Printf("错误：为服务 %s 创建标准错误管道失败: %s\n", service.Description, err)
		return
	}

	// 检查是否为该服务配置了日志文件。
	var logFile *os.File
	if service.LogFile != "" {
		// 如果配置了日志文件，则使用 os.OpenFile 打开或创建它。
		// os.O_CREATE: 如果文件不存在则创建。
		// os.O_WRONLY: 以只写模式打开。
		// os.O_APPEND: 在文件末尾追加内容。
		// 0666 是文件的权限设置。
		logFile, err = os.OpenFile(service.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
		if err != nil {
			// 如果打开文件失败，记录日志。这里我们选择继续执行，只是不写入文件。
			log.Printf("错误：为服务 %s 打开日志文件失败: %s\n", service.Description, err)
		} else {
			// 使用 defer 确保在 startService 函数退出时关闭文件句柄，以释放资源。
			defer logFile.Close()
		}
	}

	// 调用 cmd.Start() 来异步启动命令。
	// 这个函数不会等待命令执行完成，而是立即返回。
	if err := cmd.Start(); err != nil {
		log.Printf("错误：启动服务 %s 失败: %s\n", service.Description, err)
		return
	}

	// 打印服务已启动的信息，包括它的描述和由操作系统分配的进程 ID (PID)。
	fmt.Printf("已启动服务: %s (PID: %d)\n", service.Description, cmd.Process.Pid)

	// 创建一个 io.Writer 用于标准输出。
	var stdoutMultiWriter io.Writer
	if logFile != nil {
		// 如果日志文件成功打开，使用 io.MultiWriter 创建一个复合写入器。
		// 这样，写入 stdoutMultiWriter 的数据会同时被写入 os.Stdout (控制台) 和 logFile (日志文件)。
		stdoutMultiWriter = io.MultiWriter(os.Stdout, logFile)
	} else {
		// 如果没有日志文件，就只写入到控制台。
		stdoutMultiWriter = os.Stdout
	}

	// 对标准错误执行同样的操作。
	var stderrMultiWriter io.Writer
	if logFile != nil {
		stderrMultiWriter = io.MultiWriter(os.Stderr, logFile)
	} else {
		stderrMultiWriter = os.Stderr
	}

	// 启动一个新的协程来处理子进程的标准输出。
	// 这对于防止 I/O 阻塞主协程至关重要。
	go pipeOutput(stdout, stdoutMultiWriter, service.Description)
	// 启动另一个协程来处理子进程的标准错误。
	go pipeOutput(stderr, stderrMultiWriter, service.Description+"-stderr")

	// 调用 cmd.Wait() 来阻塞当前协程，直到子进程执行完成。
	// 这个函数会等待子进程退出，并返回一个错误（如果子进程以非零状态码退出）。
	if err := cmd.Wait(); err != nil {
		log.Printf("服务 %s 异常退出: %s\n", service.Description, err)
	} else {
		// 如果子进程正常退出（状态码为 0），打印成功信息。
		fmt.Printf("服务 %s 成功结束。\n", service.Description)
	}
}

// pipeOutput 函数从一个 reader (如子进程的 stdout 或 stderr 管道) 读取数据，
// 并将其写入到一个 writer (如控制台或文件)。
func pipeOutput(reader io.Reader, writer io.Writer, prefix string) {
	// 使用 bufio.NewScanner 创建一个扫描器，它可以方便地逐行读取数据。
	scanner := bufio.NewScanner(reader)
	// for-loop 会持续运行，直到管道关闭 (即子进程退出)。
	for scanner.Scan() {
		// 获取当前行文本。
		line := scanner.Text()
		// 使用 fmt.Fprintf 将带有前缀的行格式化并写入到指定的 writer。
		fmt.Fprintf(writer, "[%s] %s\n", prefix, line)
	}
}
